package ansible

import (
	"bytes"
	"errors"
	"fmt"
	"gitee.com/lllgo/gopkg/files"
	"gitee.com/lllgo/gopkg/template"
	"gitee.com/lllgo/opsctl/pkg/cmd"
	"gitee.com/lllgo/opsctl/pkg/conf"
	"gitee.com/lllgo/opsctl/pkg/tools"
	"github.com/spf13/cobra"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"
)

const (
	CmdName = "ansible"

	//playbooks注册命令
	registerFilePlaybooksKey = "playbooks"

	AnsileDelimLeft  = "${"
	AnsileDelimRight = "}"
)

// ansible属性配置
type AnsibleConfig struct {
	conf.PluginConfig
	// playbooks目录 默认是ansible目录下的playbooks
	Playbooks string
	// 默认加载playbooks目录下的文件作为命令 也推荐使用  no/false/0时不加载
	LoadBooks conf.ConfigBool
	// ansible hosts生成模板
	HostsTmpl string
	//配置文件或目录
	Config string
}

// ansible扩展metaConfig配置
type AnsibleMetaConfig struct {
	conf.MetaConfig
	// 当前执行命令
	Cmd string
}

//playbook属性配置
type PlaybookConfig struct {
	// 子命令名
	Cmd string
	//命令类型
	CmdType string
	// 对应playbook名字
	Name string
	// playbook描述信息
	Desc string
	// playbook长描述信息
	LongDesc string
	// ansible提供的var和自定义的var，会自动生成命令的flag来绑定
	Vars map[string]interface{}
}

// 命令信息描述
type cmdComment struct {
	name                string
	args                []string
	shortDesc, longDesc string
	example             string
	cmdType             conf.CommandType
}

var (
	def = AnsibleConfig{
		Playbooks:    "playbooks",
		PluginConfig: conf.PluginConfig{Register: "register.yaml"},
	}
	cmdType = []cmdComment{
		{
			name:      "add",
			args:      []string{"--skip-tags", "uninstall,stop"},
			shortDesc: "安装%s 等同ansible执行命令增加参数：--skip-tags uninstall,stop",
			cmdType:   conf.Add,
		},
		{
			name:      "remove",
			args:      []string{"-t", "stop,uninstall"},
			shortDesc: "卸载%s 等同ansible执行命令增加参数：-t stop,uninstall",
			cmdType:   conf.Remove,
		},
		{
			name:      "upgrade",
			args:      []string{"-t", "upgrade,update"},
			shortDesc: "更新%s版本 等同ansible执行命令增加参数：-t upgrade,update",
			cmdType:   conf.Upgrade,
		},
		{
			name:      "init",
			args:      []string{"-t", "init,compile,download,down"},
			shortDesc: "下载安装%s依赖的资源到本地 等同ansible执行命令增加参数：-t download,down",
			cmdType:   conf.Init,
		},
		{
			name:      "start",
			args:      []string{"-t", "start"},
			shortDesc: "启动%s服务 等同ansible执行命令增加参数：-t start",
			cmdType:   conf.Start,
		},
		{
			name:      "stop",
			args:      []string{"-t", "stop"},
			shortDesc: "停止%s服务 等同ansible执行命令增加参数：-t stop",
			cmdType:   conf.Stop,
		},
	}

	tmpPlaybookFile = filepath.Join(conf.TmpDir(), "playbook.yml")
	tmpHostFile     = filepath.Join(conf.TmpDir(), "hosts")

	tmplPlaybook = template.Config{LeftDelim: AnsileDelimLeft, RightDelim: AnsileDelimRight, DestFile: tmpPlaybookFile}

	tmplHost = template.Config{LeftDelim: AnsileDelimLeft, RightDelim: AnsileDelimRight, DestFile: tmpHostFile, Funcs: template.FuncMap{
		"host":   tools.TemplateFuncHost,
		"hostIp": tools.TemplateFuncHostIp,
	}}
)

var (
	hostsTmpl string
)

func init() {
	hostsTmpl = fmt.Sprintf(`%shost .Hosts %s
[all:vars]
local_dir=%s.LocalDir%s/%s.Cmd%s
`, AnsileDelimLeft, AnsileDelimRight, AnsileDelimLeft, AnsileDelimRight, AnsileDelimLeft, AnsileDelimRight)

	var ansibleCfgs []AnsibleConfig
	var pluginCfgs []conf.PluginConfig

	pluginConfig := conf.PluginPath
	if pluginConfig == "" {
		fmt.Println("插件配置文件为空")
		os.Exit(1)
	}
	_ = conf.BindConfig(pluginConfig, &pluginCfgs, CmdName)
	_ = conf.BindConfig(pluginConfig, &ansibleCfgs, CmdName)

	for i, cfg := range ansibleCfgs {
		cfg.PluginConfig = pluginCfgs[i]
		cfg.Dir = files.Abs(pluginConfig, cfg.Dir)
		cmd.Register(nil, createCmd(cfg))
	}
}

func createCmd(cfg AnsibleConfig) *cobra.Command {
	if _, err := os.Stat(cfg.Dir); os.IsNotExist(err) {
		fmt.Printf("ansible目录%s不存在\n", cfg.Dir)
		os.Exit(1)
	}

	cfg.Dir, _ = filepath.Abs(cfg.Dir)

	if cfg.Cmd == "" {
		cfg.Cmd = cfg.Dir[:strings.LastIndex(cfg.Dir, "/")+1]
	}

	if cfg.Playbooks == "" {
		cfg.Playbooks = def.Playbooks
	}

	if cfg.Register == "" {
		cfg.Register = def.Register
	}

	pcs, err := readPlaybooksUseRegisterFile(cfg)

	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	if conf.Bool(cfg.LoadBooks, true) {
		sb := bytes.NewBufferString("")
		for _, pc := range pcs {
			sb.WriteString(pc.Name + " ")
		}
		pcs = append(pcs, loadPlaybooks(sb.String(), cfg)...)
	}

	tmpCmd := &cobra.Command{
		Use:   cfg.Cmd,
		Short: cfg.Desc,
		Long:  cfg.Desc,
	}

	for _, pc := range pcs {
		tmpCmd.AddCommand(createSubCmd(cfg, pc)...)
	}

	return tmpCmd
}

func readPlaybooksUseRegisterFile(c AnsibleConfig) (pcs []PlaybookConfig, err error) {
	if err = conf.BindConfig(filepath.Join(c.Dir, c.Register), &pcs, registerFilePlaybooksKey); err != nil {
		return pcs, nil
	}

	for i, pc := range pcs {
		if pc.Name == "" {
			err = fmt.Errorf("必须指定playbook文件 %+v", pc)
			return
		}

		c, ct, _ := getCmdInfoUsePlaybook(pc.Name)

		if pc.Cmd == "" {
			pcs[i].Cmd = c
		}

		if pc.CmdType == "" {
			pcs[i].CmdType = ct
		}

	}

	return
}

func loadPlaybooks(exists string, c AnsibleConfig) []PlaybookConfig {
	files, err := ioutil.ReadDir(filepath.Join(c.Dir, c.Playbooks))

	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	var pcs []PlaybookConfig

	for _, f := range files {
		name := f.Name()
		if !strings.Contains(exists, name) {
			c, ct, _ := getCmdInfoUsePlaybook(name)
			pcs = append(pcs, PlaybookConfig{
				Name:    name,
				Cmd:     c,
				CmdType: ct,
			})
		}
	}

	return pcs
}

func createSubCmd(c AnsibleConfig, pc PlaybookConfig) []*cobra.Command {
	var cmds []*cobra.Command

	for _, v := range pc.getCmdComment() {

		playbook := tools.NewPlaybook(c.Dir, tmpHostFile, tmpPlaybookFile)
		playbook.Args = v.args
		playbook.Params = map[string]string{
			"ctltype": v.name,
		}

		n := v.name
		if n != "" {
			n = "-" + n
		}

		existsHostEnv := ""

		if c.Config != "" {
			cfgFile := files.Abs(conf.PluginPath, c.Config)
			for k, v := range conf.GetConfigAllExistsHostEnvs(cfgFile, c.Cmd, pc.Cmd) {
				existsHostEnv += ", " + k + ":" + v
			}
		}

		if existsHostEnv != "" {
			existsHostEnv = "\n已配置hosts和环境%s" + existsHostEnv[2:]
		}

		subCmd := &cobra.Command{
			Use:   pc.Cmd + n,
			Short: fmt.Sprintf(v.shortDesc+" "+pc.Desc+existsHostEnv, pc.Cmd),
			Long:  pc.Desc + " " + pc.LongDesc,
			RunE: func(command *cobra.Command, args []string) error {

				configs := conf.GetComponentConfig(command.Parent().Use, pc.Cmd)

				return tools.DispatchMultiConfig(configs, func(config conf.ComponentConfig) error {
					metaConfig := config.Meta()
					if err := metaConfig.MergeArgsHosts(args); err != nil {
						return err
					}

					_ = os.MkdirAll(filepath.Join(metaConfig.LocalDir, pc.Cmd), os.ModePerm)

					hostsData := AnsibleMetaConfig{Cmd: pc.Cmd, MetaConfig: metaConfig}

					metaConfig.Hosts = tools.JoinIP(cmd.GetHostsWithTag(v.cmdType, pc.Cmd, metaConfig.Hosts.([]string)...), ",")
					config[conf.MetaKey] = metaConfig
					originalPlaybook := filepath.Join(c.Dir, c.Playbooks, pc.Name)

					if err := RenderAnsibleHostsAndPlaybook(c.getHostTmpl(), originalPlaybook, hostsData, config); err != nil {
						return err
					}

					return playbook.Run()
				})

			},
		}

		cmds = append(cmds, subCmd)
	}

	return cmds
}

func getCmdInfoUsePlaybook(playbook string) (c, ct, fileType string) {
	fileType = playbook[strings.LastIndex(playbook, ".")+1:]
	c = playbook[0 : len(playbook)-len(fileType)-1]
	if i := strings.LastIndex(c, "."); i != -1 {
		ct = c[i+1:]
		c = c[0:i]
	} else {
		ct = conf.DefCommandType.String()
	}
	return
}

func (pc PlaybookConfig) getCmdComment() []cmdComment {
	switch conf.GetCommandType(pc.CmdType) {
	case conf.Service:
		return cmdType
	case conf.Kit:
		return []cmdComment{cmdType[0], cmdType[1], cmdType[2], cmdType[3]}
	case conf.Exclusive:
		return []cmdComment{{
			name:    "",
			cmdType: conf.Exclusive,
		}}
	}
	return nil
}

func (c AnsibleConfig) getHostTmpl() string {
	if c.HostsTmpl == "" {
		return hostsTmpl
	}
	return c.HostsTmpl
}

func RenderAnsibleHostsAndPlaybook(hostsTmplText, playbookFile string, hostsData, playbookData interface{}) error {
	tmplHost.TmplText = hostsTmplText
	if err := tmplHost.Write(hostsData); err != nil {
		return errors.New("渲染hosts文件失败 " + err.Error())
	}

	tmplPlaybook.TmplFile = playbookFile
	if err := tmplPlaybook.Write(playbookData); err != nil {
		return errors.New("渲染playbook文件失败 " + err.Error())
	}

	return nil
}
